import 'dart:convert';
import 'dart:io';

import 'package:cosmic_resource_downloader/unzipper.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import 'package:pool/pool.dart';

import 'cli.dart';
import 'models.dart';
import 'tui.dart';
import 'utils.dart';

class ResourceDownloader {
  final CliOptions options;
  final TUI tui;
  final List<String> failedFiles = [];

  ResourceDownloader(this.options, this.tui);

  Future<UpdateManifest> fetchManifest(String manifestUrl) async {
    tui.log('Fetching manifest from: $manifestUrl');
    // if (options.dryRun) {
    //   return UpdateManifest(
    //     env: Env(number: 'dryrun', zkAddress: 'localhost', name: 'dryrun'),
    //     webapp: {},
    //     appstore: {},
    //   );
    // }

    final client = http.Client();
    try {
      final response = await (options.timeout != null
          ? client.get(Uri.parse(manifestUrl)).timeout(options.timeout!)
          : client.get(Uri.parse(manifestUrl)));

      if (response.statusCode != 200) {
        throw Exception('Failed to load manifest: ${response.statusCode}');
      }

      final json =
          jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
      return UpdateManifest.fromJson(json);
    } finally {
      client.close();
    }
  }

  Future<List<DownloadTask>> buildDownloadTasks(
      UpdateManifest manifest, String baseUrl) async {
    final tasks = <DownloadTask>[];

    // Process webapp
    final webappPath = manifest.webapp['path'] as String?;
    final webappFiles = manifest.webapp['files'] as Map<String, dynamic>?;
    if (webappPath != null && webappFiles != null) {
      for (final entry in webappFiles.entries) {
        final filename = entry.key;
        if (isBackupFile(filename)) {
          continue;
        }
        final md5 = entry.value as String;
        final remoteUrl = '$baseUrl$webappPath/$filename';
        final localPath = p.join(options.dir, 'webapp', filename);
        tasks.add(DownloadTask(url: remoteUrl, localPath: localPath, md5: md5));
      }
    }

    // Process appstore
    final appstorePath = manifest.appstore['path'] as String?;
    if (appstorePath != null) {
      for (final section in ['bos', 'biz', 'trd', 'cus']) {
        final sectionFiles =
            manifest.appstore[section] as Map<String, dynamic>?;
        if (sectionFiles != null) {
          for (final entry in sectionFiles.entries) {
            final filename = entry.key;
            if (isBackupFile(filename)) {
              continue;
            }
            final md5 = entry.value as String;
            final remoteUrl = '$baseUrl$appstorePath/$section/$filename';
            final localPath =
                p.join(options.dir, 'appstore', section, filename);
            tasks.add(
                DownloadTask(url: remoteUrl, localPath: localPath, md5: md5));
          }
        }
      }
    }

    return tasks;
  }

  Future<void> execute() async {
    final manifestUrl = ensureUpdateJson(options.url);
    final manifest = await fetchManifest(manifestUrl);

    // Setup log file
    final logFileName =
        '${manifest.env.number}_${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.log';
    final logFilePath = p.join(options.dir, logFileName);
    await ensureDirectoryExists(options.dir);

    final logFile = File(logFilePath);
    await logFile.create();

    tui.setLogFile(logFile);
    tui.log('=== Cosmic Resource Downloader Started ===');
    tui.log('Env Number: ${manifest.env.number}');
    tui.log('ZK Address: ${manifest.env.zkAddress}');
    tui.log('Download Dir: ${options.dir}');
    tui.log('Concurrent Threads: ${options.threads}');
    tui.log('Retry Times: ${options.retries}');
    if (options.timeout != null) {
      tui.log('Timeout: ${options.timeout!.inSeconds}s');
    }
    if (options.dryRun) {
      tui.log(
          '⚠️  DRY RUN MODE - No actual downloads or unzips will be performed.');
    }
    tui.log('');

    // Build tasks
    final baseUrl = Uri.parse(options.url).replace(path: '').toString();
    final tasks = await buildDownloadTasks(manifest, baseUrl);

    tui.log('Total files to process: ${tasks.length}');
    if (options.dryRun) {
      for (final task in tasks) {
        tui.log('[DRY RUN] Would download: ${task.url} -> ${task.localPath}');
      }
      tui.log('✅ Dry run completed.');
      return;
    }

    // Create directories
    for (final task in tasks) {
      await ensureDirectoryExists(p.dirname(task.localPath));
    }

    // Filter tasks: check existing files
    final tasksToDownload = <DownloadTask>[];
    for (final task in tasks) {
      final exists = await File(task.localPath).exists();
      if (exists) {
        final localMd5 = await computeFileMd5(task.localPath);
        if (localMd5 == task.md5) {
          tui.log('✅ Skipping (MD5 match): ${p.basename(task.localPath)}');
          continue;
        } else {
          tui.log('🔄 MD5 mismatch, deleting: ${p.basename(task.localPath)}');
          await File(task.localPath).delete();
        }
      }
      tasksToDownload.add(task);
    }

    tui.log('Files to download: ${tasksToDownload.length}');

    // Download with concurrency control
    final pool = Pool(options.threads);
    int completed = 0;
    final total = tasksToDownload.length;

    for (final task in tasksToDownload) {
      pool.withResource(() async {
        bool success = false;
        int attempt = 0;
        while (!success && attempt <= options.retries) {
          attempt++;
          try {
            // tui.updateTask('Downloading ${p.basename(task.localPath)}',
            //     (completed / total * 100).toInt());
            await _downloadFile(task, attempt);
            success = true;
            completed++;
          } catch (e) {
            tui.log(
                '❌ Failed attempt $attempt for ${p.basename(task.localPath)}: $e');
            if (attempt <= options.retries) {
              await Future.delayed(Duration(seconds: 2)); // 简单退避
            } else {
              failedFiles.add(task.localPath);
            }
          }
        }
      });
    }

    await pool.close();
    tui.log('Downloads completed');

    if (failedFiles.isNotEmpty) {
      tui.log('');
      tui.log('❌ Failed files:');
      for (final file in failedFiles) {
        tui.log('  - $file');
      }
    }

    // Proceed to unzip
    await Unzipper(options, tui, manifest.env.number).execute();
  }

  Future<void> _downloadFile(DownloadTask task, int attempt) async {
    final client = http.Client();
    try {
      final response = await (options.timeout != null
          ? client.get(Uri.parse(task.url)).timeout(options.timeout!)
          : client.get(Uri.parse(task.url)));

      if (response.statusCode != 200) {
        throw Exception('HTTP ${response.statusCode}');
      }

      final file = File(task.localPath);
      final randomTemp =
          '${task.localPath}.tmp.${DateTime.now().millisecondsSinceEpoch}';
      final tempFile = File(randomTemp);

      await tempFile.create(recursive: true);
      final sink = tempFile.openWrite();
      await sink.addStream(Stream.value(response.bodyBytes));
      await sink.flush();
      await sink.close();

      // Verify MD5
      // final downloadedMd5 = await computeFileMd5(randomTemp);
      // if (downloadedMd5 != task.md5) {
      //   await tempFile.delete();
      //   throw Exception(
      //       'MD5 mismatch: expected ${task.md5}, got $downloadedMd5');
      // }

      // Replace original
      if (await file.exists()) {
        await file.delete();
      }
      await tempFile.rename(task.localPath);

      tui.log('✅ Downloaded: ${p.basename(task.localPath)} (attempt $attempt)');
    } finally {
      client.close();
    }
  }
}
